// OLFlickAndDrag
//
// OLFlickAndDrag.cpp
//
// Copyright (c) Orange Labs UK. All rights reserved.
//

#include "OLFlickAndDrag.h"

#include <QTimer>
#include <QSize>
#include <QRect>

const int LONGPRESS_TIME = 2000;		// time in ms for when a long press is detected
const int PAUSE_TIME_TO_RELEASE = 100;	// time in ms between last move and release to detect a pause
const int ERR_POS_THRESHOLD = 10;		// number of pixels to account for pointer error variations
const int ERR_TIME_THRESHOLD = 20;		// time in ms between move event to ignore below
const int STRETCH_SPEED_FACTOR = 3;		// the speed factor to spring back to min/max limits
const int MAX_SPEED_LIMIT = 1000;		// the maximum speed limit to allow in centi-pixels per millisecond
const qreal RELEASE_SPEED_FACTOR = 2.0;	// the speed factor to apply at the point at which the release occurs
const qreal DECELERATE_FACTOR = 0.90;	// the rate to decrease the speed from a flick
const int X_Y_RATIO = 3;				// the ratio that x drag should be greater than y to detect swipe
const bool STRETCH_WHEN_NO_SCROLL = false;      // stretch when no scrolling is required
const bool SNAP_AT_START = false;               // when the range changes, animate to start position

OLFlickAndDrag::OLFlickAndDrag(OLFlickAndDragInfoProvider& observer, int min, int max, int stretch, QObject *parent)
        : QObject(parent), m_observer(observer), m_min(min), m_max(max), m_state(Idle), m_stretch(stretch)
{
	m_timer = new QTimer(this);
	connect(m_timer, SIGNAL(timeout()), this, SLOT(handleTimerEvent()));
	
	m_scrollValue = m_observer.scrollValue();
        setMinMaxScrollValue(min, max);
}
	
OLFlickAndDrag::~OLFlickAndDrag()
{
}

void OLFlickAndDrag::setMinMaxScrollValue(int min, int max)
{
    // set the new max
    int scrollableArea = max-m_stretch-m_stretch;
    if(scrollableArea<=0) {
        // the scrollable area is not big enough for scrolling
        m_min = m_stretch;
        m_max = m_stretch;
    } else {
        // we can apply kinetic scrolling
        m_min = m_stretch;
        m_max = max-m_stretch;
    }

    if (m_scrollValue<=m_min || m_scrollValue>m_max) {
        if(!SNAP_AT_START) {
            m_scrollValue = m_min;
        }
        m_stopwatch.start();
        m_timer->start(20);
        m_state = m_scrollValue<=m_min ? StretchedTop : StretchedBottom;
    }
}

void OLFlickAndDrag::setStretchValue(int stretch)
{
    m_stretch = stretch;
}

void OLFlickAndDrag::rangeChange(int min, int max)
{
    setMinMaxScrollValue(min, max);
}

void OLFlickAndDrag::reset()
{
	m_timer->stop();
	m_stopwatch.start();
	m_totalTime.start();
	m_lastDiffs[0] = -1;
	m_lastDiffs[1] = -1;
	m_lastDiffs[2] = -1;
	m_direction = UnknownDirection;
	m_scrollValue = m_observer.scrollValue();
}

void OLFlickAndDrag::handleMousePress(const QPoint& pos)
{
	reset();
	m_startPos = pos;
	m_lastMovePos = m_startPos;
	
	switch(m_state)
	{
		case AutoScrolling:
			m_state = Stopped;
			break;
		case Idle:
			m_timer->setSingleShot(true);
			m_timer->start(LONGPRESS_TIME);
			// fall thru to default
		default:
			m_state = Pressed;
	}
}

void OLFlickAndDrag::handleMouseMove(const QPoint& pos)
{
	// using the pos error theshold detect if a valid click
	int x = m_lastMovePos.x()-ERR_POS_THRESHOLD;
	int y = m_lastMovePos.y()-ERR_POS_THRESHOLD;
	QPoint topLeft(x, y);
	QSize areaSize(ERR_POS_THRESHOLD*2, ERR_POS_THRESHOLD*2);
	QRect pointArea(topLeft, areaSize);
	
	int time = m_stopwatch.elapsed();
	
        // only make it a valid move if already dragging or the pos and time is out of the error thresholds
        if (time>ERR_TIME_THRESHOLD && (m_state==Dragging || (!pointArea.contains(pos)))) {
		// a valid move detected, stop the timer in case it is waiting for a long press
		if (m_state==Pressed) {
			m_timer->stop();
			m_timer->setSingleShot(false);
		}
		
                if(m_min==m_max && !STRETCH_WHEN_NO_SCROLL) {
                    // if the area is not scrollable, set the scrollvalue and do nothing further
                    m_observer.setScrollValue(m_scrollValue);
                    return;
                }

		// the pointer is dragging so move the area
		m_state = Dragging;
		
		// add some resistance if dragging in the stretch zones
		int diff = pos.y()-m_lastMovePos.y();
                m_scrollValue = qBound(m_min-m_stretch, m_scrollValue-diff, m_max+m_stretch);
		if (m_scrollValue<m_min || m_scrollValue>m_max) {
			m_scrollValue += (diff/2);
		}
		
		m_observer.setScrollValue(m_scrollValue);
		
		// restart the timing stopwatch
		m_stopwatch.start();
		
		// detect the movement direction
		DragDirection oldDirection = m_direction;
		if (diff<0) {
			m_direction = MovingDown;
			diff*=-1;
			}
		else 
			m_direction = MovingUp;
		
		// caluculate speed since last move and record the values
		int speed = (diff*1000)/time;
		m_lastDiffs[0] = m_lastDiffs[1];
		m_lastDiffs[1] = m_lastDiffs[2];
		m_lastDiffs[2] = speed;
		
		// record this as the last move
		m_lastMovePos = pos;
		
		// if a direction change is detected then reset drag
		if (oldDirection!=UnknownDirection && oldDirection!=m_direction ) {
			reset();
			m_startPos = pos;
		}
	}
}

void OLFlickAndDrag::handleMouseRelease(const QPoint& pos)
{
	switch(m_state)
	{
		case Dragging:
		{
			int elapsedTime = m_stopwatch.restart();
			
			// if we are in the stretch zones, simply spring back to the limits
			if (m_scrollValue<m_min || m_scrollValue>m_max) {
				m_timer->start(20);
				m_state = m_scrollValue<m_min ? StretchedTop : StretchedBottom;
				return;
			}
			
			// check if the user is doing a side swipe
			int distance = m_startPos.y() - pos.y();
			int xDelta = m_startPos.x() - pos.x();
			if( handleLeftOrRightSwipe(xDelta, distance) ) {
				m_state = Idle;
				return;
			}
			
			// if we enough history data to calculate if the speed is decreasing or it is detected the user held at the
			// last point, end the drag there
			int oldestDiff = m_lastDiffs[0];
			if ( oldestDiff!=-1 && (elapsedTime>PAUSE_TIME_TO_RELEASE || (oldestDiff>m_lastDiffs[1])) ) {
				m_state = Idle;
				return;
				}
			
			// do a flick based the average speed of the whole gesture
			elapsedTime = m_totalTime.elapsed();			
			if (elapsedTime>0 && distance!=0 ) {
				int speed = (distance*100) / elapsedTime;
				m_speed = qBound(-MAX_SPEED_LIMIT, speed, MAX_SPEED_LIMIT);
				m_speed *= RELEASE_SPEED_FACTOR;
				
				m_timer->start(20);
				m_state = AutoScrolling;
			}
		} break;
		case Pressed:
		{
			// stop the long hold timer
			m_timer->stop();
			
			// in case the event was in the stretch zone while it was still animating back, continue the spring back
			if (m_scrollValue<m_min || m_scrollValue>m_max) {
				m_stopwatch.start();
				m_timer->start(20);
				m_state = m_scrollValue<m_min ? StretchedTop : StretchedBottom;
				return;
			}
			
			// using the pos error theshold detect if a valid click
			int x = m_startPos.x()-ERR_POS_THRESHOLD;
			int y = m_startPos.y()-ERR_POS_THRESHOLD;
			QPoint topLeft(x, y);
			QSize areaSize(ERR_POS_THRESHOLD*2, ERR_POS_THRESHOLD*2);
			QRect pointArea(topLeft, areaSize);
			if (pointArea.contains(pos)) {
				m_observer.handleClick(m_startPos); // click detected
				}
		}	// fall thru to default state		
		case Stopped:
		default:
			m_state = Idle;
			break;
	}	
}

void OLFlickAndDrag::handleTimerEvent()
{
	switch(m_state)
	{
		case Pressed:
		{
			m_observer.handleLongPress(m_startPos);
			m_state = Idle;
		} break;
		case AutoScrolling:
		{
			// caluculate the amount to scroll
			int elapsedTime = m_stopwatch.restart();
			int scrollBy = (m_speed*elapsedTime)/100;
			
			// apply the scroll
                        m_scrollValue = qBound(m_min-m_stretch, m_scrollValue+scrollBy, m_max+m_stretch);
			m_observer.setScrollValue(m_scrollValue);
			
			if (m_scrollValue<m_min || m_scrollValue>m_max) {
				// hit the start, apply extra resistance to the stretch
				m_speed *= 0.5;
			}
			
			// calculate the new speed
			qreal speed = m_speed;
			if( speed<0 )
				speed*=-1; // make speed a postive value
				
			if( speed<5 ) {
				// speed has reached minimum
				if (m_scrollValue<m_min)
					m_state=StretchedTop;
				else if (m_scrollValue > m_max)
					m_state=StretchedBottom;
				else {
					m_timer->stop();
					m_state = Idle;
				}
			}
			else {
				m_speed *= DECELERATE_FACTOR;
				}
		} break;
		case StretchedTop:
		{
                        handleSnap(m_min-m_stretch, m_min, m_min);
		} break;
		case StretchedBottom:
		{
                        handleSnap(m_max, m_max+m_stretch, m_max);
		} break;
		default:
			break;
	}
}

void OLFlickAndDrag::handleSnap(int min, int max, int target)
{
	int elapsedTime = m_stopwatch.restart();
        int speed = (m_stretch*100)/(100*STRETCH_SPEED_FACTOR);
	int scrollBy = (speed*elapsedTime)/100;
	
	if (m_state==StretchedBottom)
		scrollBy*=-1;
	
	m_scrollValue = qBound(min, m_scrollValue+scrollBy, max);
	m_observer.setScrollValue(m_scrollValue);
	
	if( m_scrollValue==target ) {
		m_timer->stop();
		m_state=Idle;
	}
}

bool OLFlickAndDrag::handleLeftOrRightSwipe(int xdelta, int ydelta) const
{
	if (ydelta<0)
		ydelta*=-1;
		
	bool consumed = false;
	if (xdelta>ydelta*X_Y_RATIO) {
		m_observer.handleLeftSwipe();
		consumed = true;
		}
	else if (xdelta+ydelta*X_Y_RATIO<0) {
		m_observer.handleRightSwipe();
		consumed = true;
		}
		
	return consumed;
}
